import java.util.Arrays;
import java.util.Random;


/**
 * @author Simon
 * Define the Particle of the PSO.
 * Use a network to represent the position of the particle
 * Use another network to represent the velociry of the particle
 */
public class Particle {
	Data trainData;
	int inputLayerNumber;
	int hiddenLayerNumber;
	int outputLayerNumber;
	Network currentPosition;
	Network bestPosition;
	double particleBestValue;
	Network velocity;
	int iterationNumber = 0;
	double [][] v_inputWeight;
	double [] v_hiddenBias;
	double [][] v_outputWeight;
	double [] v_outputBias;
	Random seed;
	static final double vMin = -1;
	static final double vMax = 1;
	static final double xMin = -1;
	static final double xMax = 1;
	static final double c1 = 0.5;
	static final double c2 = 0.5;
	double w0 = 1;
	double w = 1;
	/**
	 * the parameter define the end condition of training iteration.
	 */
	boolean bestChanged;
	
	public Particle(Data trainData, int inputLayerNumber, int hiddenLayerNumber, int outputLayerNumber){
		seed = new Random();
		this.trainData = trainData;
		this.inputLayerNumber = inputLayerNumber;
		this.hiddenLayerNumber = hiddenLayerNumber;
		this.outputLayerNumber = outputLayerNumber;
		currentPosition = new Network(inputLayerNumber, hiddenLayerNumber, outputLayerNumber);
		if (outputLayerNumber > 2){
			currentPosition.classNumber = outputLayerNumber;
		}
		bestPosition = currentPosition;
		particleBestValue = getValueOfPosition(trainData, currentPosition);
		velocity = new Network(inputLayerNumber, hiddenLayerNumber, outputLayerNumber);
	}
	
	/**
	 * Update the inertia weight w after each iteration.
	 * @param iteration the number of current iteration
	 * @param iterationMax
	 */
	public void updateW(int iteration, int iterationMax){
		bestChanged = false;
		if (iteration < iterationMax)
			w = w0 - 0.5 * (double)iteration / iterationMax;
		else
			w = (w0 - 0.5) * Math.pow(Math.E, iteration - iterationMax);
	}
	

	public double boundaryVelocity(double v){
		if (v > vMax) return vMax;
		if (v < vMin) return vMin;
		return v;
	}
	
	public double boundaryPosition(double x){
		if (x > xMax) return xMax;
		if (x < xMin) return xMin;
		return x;
	}	
	
	public void updateVelocity(Network bestPositionOverAll){
		for (int i = 0; i < inputLayerNumber; i++){
			for (int j = 0; j < hiddenLayerNumber; j++){
				velocity.inputWeight[i][j]
				   = calNewVelociry(velocity.inputWeight[i][j], currentPosition.inputWeight[i][j]
				                   , bestPosition.inputWeight[i][j], bestPositionOverAll.inputWeight[i][j]);
				velocity.inputWeight[i][j] = boundaryVelocity(velocity.inputWeight[i][j]);
			}
		}
		
		for (int i = 0; i< hiddenLayerNumber; i++){
			velocity.hiddenBias[i]
			     				   = calNewVelociry(velocity.hiddenBias[i], currentPosition.hiddenBias[i]
			     				                   , bestPosition.hiddenBias[i], bestPositionOverAll.hiddenBias[i]);
			velocity.hiddenBias[i] = boundaryVelocity(velocity.hiddenBias[i]);
		}
		
		for (int i = 0; i < hiddenLayerNumber; i++){
			for (int j = 0; j < outputLayerNumber; j++){
				velocity.outputWeight[i][j]
				     				   = calNewVelociry(velocity.outputWeight[i][j], currentPosition.outputWeight[i][j]
				     				                   , bestPosition.outputWeight[i][j], bestPositionOverAll.outputWeight[i][j]);
				velocity.outputWeight[i][j] = boundaryVelocity(velocity.outputWeight[i][j]);
			}
		}
		
		for (int i = 0; i< outputLayerNumber; i++){
			velocity.outputBias[i]
			     				   = calNewVelociry(velocity.outputBias[i], currentPosition.outputBias[i]
			     				                   , bestPosition.outputBias[i], bestPositionOverAll.outputBias[i]);
			velocity.outputBias[i] = boundaryVelocity(velocity.outputBias[i]);
		}		
	}
	
	double calNewVelociry(double oldVelociry, double cPosition, double pbPosition, double bPosition){
		double r1 = getRandomValue();
		double r2 = getRandomValue();
		return w * oldVelociry + c1 * r1 * (pbPosition - cPosition) + c2 * r2 * (bPosition - cPosition);
	}
	
	public void updatePostion(){
		for (int i = 0; i < inputLayerNumber; i++){
			for (int j = 0; j < hiddenLayerNumber; j++){
				currentPosition.inputWeight[i][j] += velocity.inputWeight[i][j];
				currentPosition.inputWeight[i][j] = boundaryPosition(currentPosition.inputWeight[i][j]);		}
		}
		
		for (int i = 0; i< hiddenLayerNumber; i++){
			currentPosition.hiddenBias[i] += velocity.hiddenBias[i];
			currentPosition.hiddenBias[i] = boundaryPosition(currentPosition.hiddenBias[i]);
		}
		
		for (int i = 0; i < hiddenLayerNumber; i++){
			for (int j = 0; j < outputLayerNumber; j++){
				currentPosition.outputWeight[i][j] += velocity.outputWeight[i][j];
				currentPosition.outputWeight[i][j] = boundaryPosition(currentPosition.outputWeight[i][j]);
			}
		}
		
		for (int i = 0; i< outputLayerNumber; i++){
			currentPosition.outputBias[i] += velocity.outputBias[i];
			currentPosition.outputBias[i] = boundaryPosition(currentPosition.outputBias[i]);
		}
		double value = getValueOfPosition(trainData, currentPosition);
		if (value > particleBestValue){
			bestPosition = currentPosition.clone();
			particleBestValue = value;
			bestChanged = true;
		}
	}
	
	/**
	 * @return Random double from -1 to 1
	 */
	double getRandomValue(){
		return (seed.nextDouble() - 0.5) * 2;
	}
	
	/**
	 * Evaluate position of this particle by the accuracy of the position network.
	 * This method is naive and time consuming, remain to be improved further.
	 * @param trainData current dataset.
	 * @param position the network which stand for current position.
	 * @return the accuracy of the dataset.
	 */
	double getValueOfPosition(Data trainData, Network position){
		return trainData.calMisRecord(position);
	}
}
